/*
 * Copyright (c) 2020 Fraunhofer FOKUS and others. All rights reserved.
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contact: mosaic@fokus.fraunhofer.de
 */
package org.eclipse.mosaic.fed.carla.junit;

import com.google.common.collect.Lists;
import org.eclipse.mosaic.rti.api.federatestarter.ExecutableFederateExecutor;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.EachTestNotifier;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.List;

/**
 * CARLA tests rely on carla to execute.
 * Not every machine install carla, we could also set the remote ip:todo
 * This runner retries a test at most three times and only fails if all three attempts have failed referring to SumoRunner
 * Also annotated tests are skipped if carla is not find or Maven is called with {@code -DallowCarlaTests}.
 */
public class CarlaRunner extends BlockJUnit4ClassRunner {

    private static final int MAX_ATTEMPTS = 3;
    private static final String CARLA_REMOTE_IP = "192.168.195.75";
    private static final int CARLA_PORT = 12000;
    private int failedAttempts = 0;

    public CarlaRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription());
        Statement statement = classBlock(notifier);

        try {
            // skip if called with `mvn test -DskipCarlaTests` for faster builds
            Assume.assumeTrue("Skipping CARLA Test", System.getProperty("allowCarlaTests") != null);
            Assume.assumeTrue("CARLA is not find. Skipping.", startLocal());

            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.fireTestIgnored();
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            retry(testNotifier, statement, e);
        }
    }

    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        if (method.getAnnotation(Ignore.class) != null) {
            notifier.fireTestIgnored(description);
        } else {
            runTestUnit(methodBlock(method), description, notifier);
        }
    }

    /**
     * Runs a {@link Statement} that represents a leaf (aka atomic) test.
     */
    private void runTestUnit(Statement statement, Description description,
                             RunNotifier notifier) {
        EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
        eachNotifier.fireTestStarted();
        try {
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            retry(eachNotifier, statement, e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }

    private void retry(EachTestNotifier notifier, Statement statement, Throwable currentThrowable) {
        Throwable caughtThrowable = currentThrowable;
        while (MAX_ATTEMPTS > failedAttempts) {
            try {
                statement.evaluate();
                return;
            } catch (AssumptionViolatedException e) {
                notifier.addFailedAssumption(e);
                return;
            } catch (Throwable t) {
                failedAttempts++;
                caughtThrowable = t;
            }
        }
        notifier.addFailure(caughtThrowable);
    }


    private boolean startLocal() throws Exception {
        if (isCarlaReady()) {
            return Boolean.TRUE;
        }

        ExecutableFederateExecutor executor = new ExecutableFederateExecutor(null, getCarlaExecutable(),
                getProgramArguments(CARLA_PORT));
        File tmp = new File("./tmp");
        if (!tmp.exists()) {
            boolean mkdir = tmp.mkdir();
            if (!mkdir) {
                return false;
            }
        }
        executor.startLocalFederate(tmp);

        return isCarlaReady();
    }

    private boolean isCarlaReady() throws Exception {
        Process process = Runtime.getRuntime().exec("netstat -nlp | grep CarlaUE4 | grep " + CARLA_PORT);
        String line = "";
        StringBuilder message = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        while ((line = reader.readLine()) != null) {
            message.append(line);
        }

        return message.toString().contains("CarlaUE4");
    }


    private String getCarlaExecutable() {
        String carlaHome = null;
        String executable = "CarlaUE4";
        if (System.getenv("CARLA_HOME") != null) {
            carlaHome = System.getenv("CARLA_HOME");
        } else {
            return null;
        }

        boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
        if (isWindows) {
            executable += ".exe";
        } else {
            executable += ".sh";
        }
        return carlaHome + File.separator + executable;
    }

    private List<String> getProgramArguments(int port) {

        List<String> args = Lists.newArrayList(
                "-prefernvidia",
                "-carla-rpc-port=" + port);

        return args;
    }
}
